/*
 * Copyright 2006-2008 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.martinimix.service.account;

import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import net.martinimix.dao.account.AccountDao;
import net.martinimix.domain.account.Address;
import net.martinimix.domain.account.Customer;
import net.martinimix.domain.account.CustomerNotFoundException;
import net.martinimix.domain.account.InvalidLoginException;
import net.martinimix.domain.payment.Account;
import net.martinimix.domain.payment.BillMeLaterAccount;
import net.martinimix.domain.payment.CreditCard;
import net.martinimix.domain.payment.CreditCardNumberComparator;
import net.martinimix.domain.payment.CreditCardToolkit;
import net.martinimix.domain.payment.NonUniqueCreditCardException;
import net.martinimix.domain.payment.PaymentMethod;
import net.martinimix.service.payment.CreditCardNotFoundException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.bluemartini.dna.BMSecurity;
import com.bluemartini.dna.BMThreadManager;

/**
 * Provides a MartiniMix customer service implementation.
 * 
 * @author Scott Rossillo
 *
 */
public class MartiniMixCustomerService implements CustomerService {
	
	private final static Log log = LogFactory.getLog(MartiniMixCustomerService.class);
	
	private CreditCardNumberComparator creditCardNumberComparator = new CreditCardNumberComparator();
	
	/**
	 * The account data access object for this service.
	 */
	private AccountDao accountDao;
	
	private boolean allowDuplicateCreditCards;
	
	/**
	 * Creates a new Martini Mix customer service.
	 */
	public MartiniMixCustomerService() { 
		this.allowDuplicateCreditCards = false;
	}
	
	/* (non-Javadoc)
	 * @see net.martinimix.domain.account.CustomerService#createGuestCustomer()
	 */
	public Customer createGuestCustomer() {
		
		return accountDao.createGuestCustomer();
	}
	
	/* (non-Javadoc)
	 * @see net.martinimix.service.account.CustomerService#getBillingAddresses(long)
	 */
	public List getBillingAddresses(long customerId) {
		
		Customer customer = accountDao.findCustomer(customerId);
		List addresses = accountDao.getBillingAddresses(customerId);
		
		setDefaultAddress(customer.getDefaultBillingAddressId(), addresses);
		
		return addresses;
	}
	
	/* (non-Javadoc)
	 * @see net.martinimix.service.account.CustomerService#getCustomerShippingAddresses(long)
	 */
	public List getShippingAddresses(long customerId) {
		
		Customer customer = accountDao.findCustomer(customerId);
		List addresses = accountDao.getShippingAddresses(customerId);
		
		setDefaultAddress(customer.getDefaultShippingAddressId(), addresses);
		
		return addresses;
	}
	
	/**
	 * Sets default to true on the default address in the given list.
	 */
	private void setDefaultAddress(Long defaultId, List addresses) {
		
		if(defaultId == null || addresses == null) {
			return;
		}
		
		Address address;
		for(int i = 0; i < addresses.size(); i++) {
			address = (Address) addresses.get(i);
			if(defaultId.equals(address.getId())) {
				address.setDefaultAddress(true);
				break;
			}
		}
		
		Collections.sort(addresses, new DefaultAddressComparator());
	}

	
	/* (non-Javadoc)
	 * @see net.martinimix.domain.account.CustomerService#login(java.lang.String, java.lang.String)
	 */
	public Customer login(String username, String password) throws InvalidLoginException {
		
		Customer customer = accountDao.login(username, password); 
		
		if(log.isDebugEnabled()) {
			log.debug("Logged in customer: " + customer);
		}
		
		BMThreadManager.setUserAccountID(customer.getId());
		
		return customer;
	}
	
	/* (non-Javadoc)
	 * @see net.martinimix.domain.account.CustomerService#findAddress(long)
	 */
	public Address findAddress(long id) throws AddressNotFoundException{
		
		if(log.isDebugEnabled()) {
			log.debug("Finding address with id [" + id + "]");
		}
		
		Address address = accountDao.findAddress(id); 
		
		if(address == null) {
			throw new AddressNotFoundException("Address with id [" + id + "] not found!");
		}
		
		if(address.getCountry() == null) {
			address.setCountry("US");
		}
		
		// only go through this if the DAO didn't know how to set it
		if(!address.isDefaultAddress()) {
			Customer customer = accountDao.findCustomer(address.getCustomerId().longValue());
			
			if(Address.TYPE_BILLING.equals(address.getTypeCode())) {
				address.setDefaultAddress(address.getId().equals(customer.getDefaultBillingAddressId()));
			} else if(Address.TYPE_SHIPPING.equals(address.getTypeCode())) {
				address.setDefaultAddress(address.getId().equals(customer.getDefaultShippingAddressId()));
			}	
		}
		
		return address;
	}
	
	/* (non-Javadoc)
	 * @see net.martinimix.service.account.CustomerService#findCustomer(long)
	 */
	public Customer findCustomer(long customerId) throws CustomerNotFoundException {
		
		Customer customer = accountDao.findCustomer(customerId);
		
		if(customer == null) {
			throw new CustomerNotFoundException("Unkown customer id [" + customerId + "]");
		}
		
		return customer;
	}

	/* (non-Javadoc)
	 * @see net.martinimix.service.account.CustomerService#findCustomer(java.lang.String)
	 */
	public Customer findCustomer(String username) throws CustomerNotFoundException {
		
		Customer customer = accountDao.findCustomer(username);
		
		if(customer == null) {
			throw new CustomerNotFoundException("Unkown customer username [" + username + "]");
		}
		
		return customer;
	}
	
	/* (non-Javadoc)
	 * @see net.martinimix.service.account.CustomerService#findCreditCard(net.martinimix.domain.account.Customer, java.lang.String)
	 */
	public CreditCard findCreditCard(Customer customer, String cardNumber) throws CreditCardNotFoundException {
		
		final Iterator it = this.getCreditCards(customer).iterator();
		CreditCard creditCard;
		
		if(cardNumber == null) {
			throw new IllegalArgumentException("[cardNumer] cannot be null!");
		}
		
		while(it.hasNext()) {
			creditCard = (CreditCard) it.next();
			if(cardNumber.equals(creditCard.getCardNumber())) {
				return creditCard;
			}
		}
		
		throw new CreditCardNotFoundException("No credit card with given card number exists!");
	}

	/* (non-Javadoc)
	 * @see net.martinimix.service.account.CustomerService#registerCustomer(net.martinimix.domain.account.Customer)
	 */
	public void registerCustomer(Customer customer) {
		
		final String password = customer.getPassword();
		
        if (password != null) {
            customer.setPassword(BMSecurity.getSHA1Hash(password));
            customer.setPasswordExpired(false);
            customer.setPasswordChangeDate(null); // how BM knows to chg the passwd (lame)
        }
		
        this.saveCustomer(customer);
	}

	/* (non-Javadoc)
	 * @see net.martinimix.domain.account.CustomerService#saveAddress(net.martinimix.domain.account.Customer, net.martinimix.domain.account.Address)
	 */
	public Address saveAddress(Customer customer, Address address) {
		
		final Address savedAddress;
		Long addressId = address.getId() != null ? new Long(address.getId().longValue()) : null; 
		
		if(address.getStatusCode() == null) {
			address.setStatusCode(Address.STATUS_ENABLED);
		}
		
		if(address.getCountry() == null) {
			address.setCountry("US");
		}
		
		address.setCustomerId(customer.getId());
		savedAddress = accountDao.saveAddresss(address);
		
		if(address.isDefaultAddress()) {
			
			if(Address.TYPE_BILLING.equals(address.getTypeCode())) {
				customer.setDefaultBillingAddressId(savedAddress.getId());
			} else if(Address.TYPE_SHIPPING.equals(address.getTypeCode())) {
				customer.setDefaultShippingAddressId(savedAddress.getId());
			}
			accountDao.saveCustomer(customer);
		}
		
		if(Address.TYPE_BILLING.equals(address.getTypeCode()) && addressId != null) {
			// BM created a new address id, so we may have to update credit cards
			this.updateCreditCardBillingAddresses(customer, addressId, savedAddress.getId());
		}
		
		return savedAddress;
	}

	/* (non-Javadoc)
	 * @see net.martinimix.domain.account.CustomerService#saveBillingAddress(net.martinimix.domain.account.Customer, net.martinimix.domain.account.Address)
	 */
	public Address saveBillingAddress(Customer customer, Address address) {
		 
		address.setTypeCode(Address.TYPE_BILLING);
		return saveAddress(customer, address);
	}

	/* (non-Javadoc)
	 * @see net.martinimix.domain.account.CustomerService#saveShippingingAddress(net.martinimix.domain.account.Customer, net.martinimix.domain.account.Address)
	 */
	public Address saveShippingAddress(Customer customer, Address address) {
		
		address.setTypeCode(Address.TYPE_SHIPPING);
		return saveAddress(customer, address);
	}

	/* (non-Javadoc)
	 * @see net.martinimix.service.account.CustomerService#saveCustomer(net.martinimix.domain.account.Customer)
	 */
	public void saveCustomer(Customer customer) {
				
		if(customer == null) {
			throw new IllegalArgumentException("[customer] to save may not be null!");
		}
		
		// TODO: enforce constraints
		if(customer.getUsername() != null) {
			customer.setUsername(customer.getUsername().toLowerCase());
		}
		
		if(customer.getEmailAddress() != null) {
			customer.setEmailAddress(customer.getEmailAddress().toLowerCase());
		}
		
		accountDao.saveCustomer(customer);
	}
	
	/* (non-Javadoc)
	 * @see net.martinimix.service.account.CustomerService#updateCreditCardBillingAddress(net.martinimix.domain.payment.CreditCard, net.martinimix.domain.account.Address, net.martinimix.domain.account.Customer)
	 */
	public CreditCard updateCreditCardBillingAddress(
			final CreditCard creditCard, 
			final Address billingAddress, 
			final Customer customer) throws CreditCardNotFoundException {
		
		if(creditCard == null || creditCard.getCardNumber() == null) {
			throw new IllegalArgumentException("[CreditCard] null or missing card number.");
		}
		
		if(billingAddress == null || billingAddress.getId() == null) {
			throw new IllegalArgumentException("[Address] null or missing id.");
		}
		
		if(customer == null || customer.getId() == null) {
			throw new IllegalArgumentException("[Customer] null or missing id.");
		}
		
		final CreditCard onFileCreditCard = this.findCreditCard(customer, creditCard.getCardNumber());
		
		if(creditCard.getBillingAddressId().equals(billingAddress.getId())) {
			// no change required
			return onFileCreditCard;
		}
		
		onFileCreditCard.setBillingAddressId(billingAddress.getId());
		this.saveCreditCard(onFileCreditCard, customer);
		
		// lookup the updated card (id will have changed and return it)
		return this.findCreditCard(customer, onFileCreditCard.getCardNumber());
	}

	/**
	 * Updates the credit card billing addresses for the given customer.
	 * 
	 * @param customer the <code>Customer</code> whose credit card billing addresses 
	 * should be updated
	 * 
	 * @param oldBillToId the old bill to <code>Address</code> identifier to replace
	 * 
	 * @param newBillToId the new bill to <code>Address</code> identifier
	 */
	protected final void updateCreditCardBillingAddresses(Customer customer, Long oldBillToId, Long newBillToId) {
		
		List creditCards;
		CreditCard creditCard;
		
		if(oldBillToId == null) {
			throw new IllegalArgumentException("[oldBillToId] cannot be null!");
		}
		
		try {
			creditCards = getCreditCards(customer);
		} catch (CreditCardNotFoundException e) {
			// no cards to update :)
			return;
		}
		
		for(Iterator it = creditCards.iterator(); it.hasNext(); ) {
			creditCard = (CreditCard) it.next();
			if(oldBillToId.equals(creditCard.getBillingAddressId())) {
				creditCard.setBillingAddressId(newBillToId);
				saveCreditCard(creditCard, customer);
			}
		}
		
	}
	
	/* (non-Javadoc)
	 * @see net.martinimix.service.payment.CreditCardPaymentService#getCreditCard(long)
	 */
	public CreditCard getCreditCard(long creditCardId) {
		return accountDao.getCreditCard(creditCardId);
	}
	
	public List getCreditCards(Customer customer) throws CreditCardNotFoundException {
		
		List cards = accountDao.getCreditCards(customer.getId().longValue()); 
		
		if(cards == null || cards.size() < 1) {
			throw new CreditCardNotFoundException(
					"Customer [" + customer + "] does not have any credit cards");
		}
		
		setDefaultCreditCard(customer.getDefaultCreditCardId(), cards);
		
		return cards;
	}
	
	/* (non-Javadoc)
	 * @see net.martinimix.service.account.CustomerService#saveAccount(net.martinimix.domain.payment.Account, net.martinimix.domain.account.Customer)
	 */
	public void saveAccount(Account account, Customer customer) {
		
		account.setCustomerId(customer.getId());
		accountDao.saveAccount(account, customer);
	}
	
	/** 
	 * Saves the given credit card on the given customer's account.
	 * 
	 * @throws NonUniqueCreditCardException if the given <code>creditCard</code> already
	 * exists for the given <code>customer</code>
	 * 
	 * @see net.martinimix.service.account.CustomerService#saveCreditCard(net.martinimix.domain.payment.CreditCard, net.martinimix.domain.account.Customer)
	 */
	public void saveCreditCard(CreditCard creditCard, Customer customer) {
		
		if(customer == null) {
			throw new IllegalArgumentException("[customer] cannot be null!");
		}
		
		if(customer.getId() == null) {
			throw new IllegalArgumentException("[customer.id] cannot be null!");
		}
		
		if(creditCard == null) {
			throw new IllegalArgumentException("[creditCard] cannot be null!");
		}
		
		creditCard.setCreatedBy(customer.getId());
		
		if(creditCard.getCardholderName() == null && 
				(customer.getFirstName() != null && customer.getLastName() != null)) {
			creditCard.setCardholderName(customer.getFirstName() + " " + customer.getLastName());
		}
		
		if(creditCard.getBrand() == null) {
			// figure it out from the card number
			creditCard.setBrand(CreditCardToolkit.getBrand(creditCard.getCardNumber()));
		}
		
		if(!this.isAllowDuplicateCreditCards()) {
			ensureCreditCardUnique(creditCard, customer);
		}
		
		accountDao.saveCreditCard(customer, creditCard);
	}
	
	/**
	 * Guarantees that the given credit card is not already saved for the given customer.
	 * 
	 * @param creditCard the <code>CreditCard</code> to ensure is unique
	 * @param customer the <code>Customer</code> for which to ensure the given
	 * <code>creditCard</code> is unique
	 * 
	 * @throws NonUniqueCreditCardException if the given <code>creditCard</code> already
	 * exists for the given <code>customer</code>
	 */
	protected final void ensureCreditCardUnique(CreditCard creditCard, Customer customer) {
		
		List creditCardsOnFile = accountDao.getCreditCards(customer.getId().longValue());
		CreditCard creditCardOnFile;
		
		for(int i = 0; i < creditCardsOnFile.size(); i++) {
			creditCardOnFile = (CreditCard) creditCardsOnFile.get(i);
			if(creditCardNumberComparator.compare(creditCardOnFile, creditCard) == 0 
					&& !creditCardOnFile.getId().equals(creditCard.getId())) {
				throw new NonUniqueCreditCardException(creditCardOnFile, creditCard);
			}
		}
	}
	
	/* (non-Javadoc)
	 * @see net.martinimix.service.account.CustomerService#saveBillMeLaterAccount(net.martinimix.domain.payment.BillMeLaterAccount, net.martinimix.domain.account.Customer)
	 */
	public void saveBillMeLaterAccount(BillMeLaterAccount billMeLaterAccount, Customer customer) {
		
		if(customer == null) {
			throw new IllegalArgumentException("[customer] cannot be null!");
		}
		
		if(customer.getId() == null) {
			throw new IllegalArgumentException("[customer.id] cannot be null!");
		}
		
		if(billMeLaterAccount == null) {
			throw new IllegalArgumentException("[billMeLaterAccount] cannot be null!");
		}
		
		billMeLaterAccount.setCreatedBy(customer.getId());
		
		if(billMeLaterAccount.getCardholderName() == null && 
				(customer.getFirstName() != null && customer.getLastName() != null)) {
			billMeLaterAccount.setCardholderName(customer.getFirstName() + " " + customer.getLastName());
		}
		
		
		if(billMeLaterAccount.getCardNumber() == null) {
			// figure it out from the card number
			// FIME: needed?
			billMeLaterAccount.setCardNumber("4111111111111111");
		}
		
		accountDao.saveCreditCard(customer, billMeLaterAccount);	
	}
	
	/* (non-Javadoc)
	 * @see net.martinimix.service.account.CustomerService#savePaymentMethod(net.martinimix.domain.payment.PaymentMethod, net.martinimix.domain.account.Customer)
	 */
	public void savePaymentMethod(PaymentMethod paymentMethod, Customer customer) {
		
		if(BillMeLaterAccount.class.isAssignableFrom(paymentMethod.getClass())) {
			this.saveBillMeLaterAccount((BillMeLaterAccount) paymentMethod, customer);
		} else if(CreditCard.class.isAssignableFrom(paymentMethod.getClass())) {
			this.saveCreditCard((CreditCard) paymentMethod, customer);
		} else {
			this.saveAccount((Account) paymentMethod, customer);
		}
	}

	private void setDefaultCreditCard(Long defaultId, List creditCards) {
		
		if(defaultId == null || creditCards == null) {
			return;
		}
		
		CreditCard address;
		for(int i = 0; i < creditCards.size(); i++) {
			address = (CreditCard) creditCards.get(i);
			if(defaultId.equals(address.getId())) {
				address.setDefaultCreditCard(true);
				break;
			}
		}
		
		Collections.sort(creditCards, new DefaultCreditCardComparator());
	}


	/**
	 * Sets the account data access object for this service.
	 * 
	 * By default, duplicate credit cards are not permitted.
	 * 
	 * @param accountDao the <code>AccountDao</code> for this
	 * <code>CustomerService</code>
	 */
	public void setAccountDao(AccountDao accountDao) {
		this.accountDao = accountDao;
	}

	/**
	 * Returns true if this customer service allows customers to save duplicate
	 * credit cards to their account.
	 * 
	 * By default, duplicate credit cards are not permitted.
	 * 
	 * @return <code>true</code> if duplicate credit cards are permitted for
	 * a customer account; <code>false</code> otherwise 
	 */
	public boolean isAllowDuplicateCreditCards() {
		return allowDuplicateCreditCards;
	}

	public void setAllowDuplicateCreditCards(boolean allowDuplicateCreditCards) {
		this.allowDuplicateCreditCards = allowDuplicateCreditCards;
	}

}
